#!/bin/sh

# Shell Script for Auto Updating the Nginx Bad Bot Blocker
# Copyright: https://github.com/mitchellkrogza
# Project Url: https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker
# Update script & Alpine Linux package by Stuart Cardall: https://github.com/itoffshore

# MAKE SURE you have all the following files in /etc/nginx/bots.d/ folder
# ***********************************************************************
# whitelist-ips.conf
# whitelist-domains.conf
# blacklist-user-agents.conf
# bad-referrer-words.conf
# custom-bad-referrers.conf
# blacklist-ips.conf
# A major change to using include files was introduced in
# https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/commit/7e3ab02172dafdd524de5dd450a9732328622779
# **************************************************************************
# Nginx will fail a reload with [EMERG] without the presence of these files.

# PLEASE READ UPDATED CONFIGURATION INSTRUCTIONS BEFORE USING THIS

# Save this file as /usr/local/sbin/update-ngxblocker
# cd /usr/local/sbin
# sudo wget https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/update-ngxblocker -O update-ngxblocker
# Make it Executable chmod 700 /usr/local/sbin/update-ngxblocker

# RUN THE UPDATE
# Here our script runs, pulls the latest update, reloads nginx and emails you a notification

EMAIL="me@myemail.com"
SEND_EMAIL="N"
SEND_EMAIL_UPDATE="N"
CONF_DIR=/etc/nginx/conf.d
BOTS_DIR=/etc/nginx/bots.d
INSTALLER=/usr/local/sbin/install-ngxblocker

##### end user configuration ##############################################################

BOLDGREEN="\033[1m\033[32m"
BOLDMAGENTA="\033[1m\033[35m"
BOLDRED="\033[1m\033[31m"
BOLDYELLOW="\033[1m\033[33m"
BOLDWHITE="\033[1m\033[37m"
RESET="\033[0m"

usage() {
        local script=$(basename $0)
        cat <<EOF
$script: UPDATE Nginx Bad Bot Blocker blacklist in: [ $CONF_DIR ]

Usage: $script [OPTIONS]
        [ -c ] : NGINX conf directory          (default: $CONF_DIR)
        [ -b ] : NGINX bots directory          (default: $BOTS_DIR)
        [ -i ] : Change installer path         (default: $INSTALLER)
        [ -r ] : Change repo url               (default: $REPO)
        [ -e ] : Change @email address         (default: $EMAIL)
        [ -m ] : Change mail (system alias)    (default: $EMAIL)
        [ -n ] : Do not send email report      (default: $SEND_EMAIL)
        [ -o ] : Only send email on update     (default: $SEND_EMAIL_UPDATE)
        [ -q ] : Suppress non error messages
        [ -v ] : Print blacklist version
        [ -h ] : this help message

Examples:
 $script                         (Download globalblacklist.conf to: $CONF_DIR)
 $script -c /my/custom/conf.d    (Download globalblacklist.conf to a custom location)
 $script -b /my/custom/bots.d    (Download globalblacklist.conf & update with your custom bots.d location)
 $script -e you@youremail.com    (Download globalblacklist.conf specifying your email address for the notification)
 $script -q -m webmaster         (Send mail to a system alias address & give less verbose messages for cron)
 $script -o -e you@youremail.com (Send mail notification only on updates)
 $script -i /path/to/install-ngxblocker (Use custom path to install-ngxblocker to update bots.d / conf.d include files)
EOF
        exit 0
}

check_version() {
	local remote_ver= remote_date= version= date= file=$CONF_DIR/globalblacklist.conf
	local tmp=$(mktemp) url=$REPO/conf.d/globalblacklist.conf range="145-345"

	if [ -f $file ]; then
		# local version
		version=$(grep "Version:" $file | sed 's|^.*: V||g')
		date=$(grep "Updated:" $file | sed 's|^.*: ||g')
		print_message "\nLOCAL Version: $BOLDWHITE$version$RESET\n"
		print_message "Updated: $date\n\n"

		# remote version
		curl -s --limit-rate 5k -r $range --location $url -o $tmp
		remote_ver=$(grep "Version:" $tmp | sed 's|^.*: V||g')
		remote_date=$(grep "Updated:" $tmp | sed 's|^.*: ||g')
		print_message "REMOTE Version: $BOLDWHITE$remote_ver$RESET\n"
		print_message "Updated: $remote_date\n"
		rm -f $tmp

		if [ "$version" != "$remote_ver" ]; then
			print_message "\nUpdate Available => $BOLDMAGENTA$remote_ver$RESET\n\n"
			return 1
		else
			print_message "\nLatest Blacklist Already Installed: $BOLDGREEN$version$RESET\n\n"
		fi
	else
		printf "${BOLDRED}ERROR${RESET}: Missing '$file' => ${BOLDWHITE}running $INSTALLER:${RESET}\n"
		$INSTALL_INC
		if [ -f $file ]; then
			check_version
		fi
	fi
}

check_dirs() {
	local x= dirs="$*"

	for x in $dirs; do
		if [ ! -d $x ]; then
			printf "${BOLDRED}ERROR${RESET}: Missing directory: $x => ${BOLDWHITE}running $INSTALLER:${RESET}\n"
			$INSTALL_INC
		fi
	done
}

update_paths() {
	# variables in nginx include files not currently possible
	# updates hard coded bots.d path in globalblacklist.conf
	local blacklist=$1 include_paths= dir= x=

	if ! grep "$BOTS_DIR" $blacklist 1>/dev/null; then
		if [ -d $BOTS_DIR ]; then
			printf "${BOLDGREEN}Updating bots.d path${RESET}: ${BOLDWHITE}$BOTS_DIR => $blacklist${RESET}\n"
			include_paths=$(grep -E "include /.*.conf;$" $blacklist | awk '{print $2}' | tr -d ';')

			for x in $include_paths; do
				dir=$(dirname $x)
				sed -i "s|$dir|$BOTS_DIR|" $blacklist
			done
		else
			printf "${BOLDRED}ERROR${RESET}: '$BOTS_DIR' does not exist => ${BOLDWHITE}running $INSTALLER${RESET}.\n"
			$INSTALL_INC
			update_paths $blacklist
		fi
	fi
}

service_cmd() {
	# arch linux does not have a 'service' command
	local x= svc= svc_list="service systemctl rc-service"

	for x in $svc_list; do
		svc=$(which $x 2>/dev/null)
		if [ -n "$svc" ]; then
			case "$x" in
				service) svc="$svc nginx reload";;
				systemctl) svc="$svc reload nginx.service";;
				rc-service) svc="$svc nginx reload";;
			esac
			break
		else
			# centos does not have 'which' by default
			svc="/usr/sbin/service nginx reload"
		fi
	done

	echo $svc
}

sanitize_path() {
	echo $1 |tr -cd '[:alnum:] [=@=] [=.=] [=-=] [=/=] [=_=]' \
		|tr -s '@.-/_' |awk '{print tolower($0)}'
}

sanitize_url() {
	echo $1 |tr -cd '[:alnum:] [=:=] [=.=] [=-=] [=/=]' \
		|tr -s ':.-' |awk '{print tolower($0)}'
}

sanitize_email() {
	echo $1 |tr -cd '[:alnum:] [=@=] [=.=] [=-=] [=_=] [=+=]' \
		|tr -s '@-_.+' |awk '{print tolower($0)}'
}

check_args() {
	local option=$1 type=$2 arg=$3
	local msg="ERROR: option '-$option' argument '$arg' requires:"

	case "$type" in
	        path)   if ! echo $arg | grep ^/ 1>/dev/null; then
				printf "$msg absolute path.\n"
				exit 1
			fi
			;;
	       email)   if ! echo $arg | grep -E ^[-+_\.[:alnum:]]+@[-_\.[:alnum:]]+ 1>/dev/null; then
				printf "$msg email@domain.com\n"
				exit 1
			fi
			;;
	         url)   if ! echo $arg | grep -E ^http[s]?://[0-9a-zA-Z-]+[.]+[/0-9a-zA-Z.]+ 1>/dev/null; then
				printf "$msg url => http[s]://the.url\n"
				exit 1
			fi
			;;
	      script)	if [ ! -x $arg ]; then
				printf "$msg '$arg' is not executable / does not exist.\n"
				exit 1
			fi
			;;
	        none)   printf "$msg argument.\n"; exit 1;;
        esac
}

check_mail_depends() {
	if [ ! -f /usr/bin/mail ] && [ ! -f /bin/mail ]; then # mailx + ssmtp are enough to send emails
		printf "${BOLDYELLOW}WARN${RESET}: missing mail command => ${BOLDWHITE}disabling emails${RESET}.\n\n"
		return 1
	fi
}

check_depends() {
	# centos does not have which by default
	if [ ! -x /usr/bin/curl ]; then
		printf "${BOLDRED}ERROR${RESET}: $0 requires: 'curl' => ${BOLDWHITE}cannot check remote version.${RESET}\n"
		exit 1
	fi

	# install-ngxblocker downloads missing scripts / includes as part of the update process
	if [ ! -x $INSTALLER ]; then
		printf "${BOLDRED}ERROR${RESET}: $0 requires: '$INSTALLER' => ${BOLDWHITE}cannot update includes.${RESET}\n"
		exit 1
	fi
}

print_message() {
	local msg="$@"

	if [ "$VERBOSE" != "N" ]; then
		printf "$msg"
	fi
}

send_email() {
	# email report
	if check_mail_depends; then
		print_message "Emailing report to: ${BOLDWHITE}$EMAIL${RESET}\n\n";
		# remove ansi colour codes
		sed -i 's/\x1b\[[0-9;]*m//g' $EMAIL_REPORT
		cat $EMAIL_REPORT | mail -s "Nginx Bad Bot Blocker Updated" $EMAIL
		rm -f $EMAIL_REPORT
	fi
}

get_options() {
	local arg= opts=

	while getopts :c:b:i:r:e:m:novqh opts "$@"
	do
		if [ -n "${OPTARG}" ]; then
			case "$opts" in
				r) arg=$(sanitize_url ${OPTARG});;
				e) arg=$(sanitize_email ${OPTARG});;
				*) arg=$(sanitize_path ${OPTARG});;
			esac
		fi

		case "$opts" in
			c) CONF_DIR=$arg; check_args $opts path $arg ;;
			b) BOTS_DIR=$arg; check_args $opts path $arg ;;
			i) INSTALLER=$arg; check_args $opts script $arg ;;
			r) REPO=$arg; check_args $opts url $arg ;;
			e) EMAIL=$arg; SEND_EMAIL=Y; check_args $opts email $arg ;;
			m) EMAIL=$arg; SEND_EMAIL=Y ;; # /etc/aliases no sanity checks
			n) SEND_EMAIL=N ;;
			o) SEND_EMAIL_UPDATE=Y ;;
			v) check_version; exit 0 ;;
			q) export VERBOSE=N ;;
			h) usage ;;
			\?) usage ;;
			:) check_args $OPTARG none none ;;
		esac
	done

	INSTALL_INC="$INSTALLER -b $BOTS_DIR -c $CONF_DIR -x"
}

main() {
	local REPO=https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master
	local file=globalblacklist.conf remote_dir=conf.d url= output= update= status= tmp= retval=
	local reload_service="$(service_cmd)"

	# require root
	if [ "$(id -u)" != "0" ]; then
		echo "This script must be run as root" 1>&2
		exit 1
	fi

	# parse command line
	get_options $@
	check_depends
	check_dirs $BOTS_DIR $CONF_DIR
	url=$REPO/$remote_dir/$file
	output=$CONF_DIR/$file

	# check for updated blacklist
	check_version
	update=$?

	if [ $update = 1 ]; then

		# download globalblacklist update
		tmp=$(mktemp)
		mkdir -p $CONF_DIR
		local dl_msg="${BOLDWHITE}Downloading: $file "
		curl --fail --connect-timeout 60 --retry 10 --retry-delay 5 -so $tmp $url
		retval=$?

		case "$retval" in
			 0) print_message "$dl_msg...${BOLDGREEN}[OK]${RESET}\n\n"
			    mv $tmp $output
			    ;;
			22) printf "$dl_msg...${BOLDRED}ERROR 404: $url${RESET}\n\n";;
			28) printf "$dl_msg...${BOLDRED}ERROR TIMEOUT: $url${RESET}\n\n";;
			 *) printf "$dl_msg...${BOLDRED}ERROR CURL: ($retval){RESET}\n\n";;
		esac

		# download new bots.d / conf.d files
		$INSTALL_INC

		# set custom bots.d path
		update_paths $output

		# re-read nginx configuration
		if [ $retval = 0 ]; then

			$reload_service 2>&1 >/dev/null
			if [ $? = 0 ]; then
				status="${BOLDGREEN}[OK]${RESET}"
				print_message "\nReloading NGINX configuration...$status\n"

			else
				status="${BOLDRED}[FAILED]${RESET}"
				printf "\nReloading NGINX configuration...$status\n"

			fi
		else
			printf "\n${BOLDRED}Download failed${RESET}: not reloading NGINX config\n"
		fi

		# in silent mode print a single message after an update
		if [ "$VERBOSE" = "N" ]; then
			printf "NGINX Blacklist updated =>$(grep "Version:" $CONF_DIR/globalblacklist.conf | tr -d '#')\n"
		fi

		# enable update only email
		if [ "$SEND_EMAIL_UPDATE" = "Y" ] ; then
			SEND_EMAIL=Y
		fi

	else
		# set custom bots.d path
		update_paths $output

		# disable update only email
		if [ "$SEND_EMAIL_UPDATE" = "Y" ] ; then
			SEND_EMAIL=N
		fi
	fi

	# email report
	case "$SEND_EMAIL" in
		y*|Y*) send_email;;
	esac
}

## start ##
EMAIL_REPORT=$(mktemp)
main $@ | tee $EMAIL_REPORT

exit $?

# Add this as a cron to run daily / weekly as you like
# Here's a sample CRON entry to update every day at 10pm
# 00 22 * * * sudo /usr/local/sbin/update-ngxblocker -q

# Here's another example to run it daily at midday using a command line switch to set the email address for the notification
# 00 12 * * * sudo /usr/local/sbin/update-ngxblocker -e yourname@youremailprovider.com

# Less verbose logging to a system alias mail address (root crontab)
# 00 12 * * * /usr/local/sbin/update-ngxblocker -q -m webmaster

# better logging for cron jobs:
# https://serverfault.com/questions/137468/better-logging-for-cronjobs-send-cron-output-to-syslog
